import os
import sys, traceback
import logging
import re
from optparse import OptionParser

from webob import Request, Response, exc
from wsgiref.simple_server import make_server
from fusion.db.db import DB
from fusion.db.util import get_default_path

class RouterException(Exception): pass


#[TODO: is it a good idea to open/close the db for every request?]
class DBMiddleware(object):
    def __init__(self, path):
        self.application = None
        self.path = path

    def set_application(self, application):
        self.application = application

    def __call__(self, environ, start_response):
        try:
            environ['fusion.db.path'] = self.path
            environ['fusion.db'] = DB(self.path)
            return self.application(environ, start_response)
        finally:
            environ['fusion.db'].close()
            pass


class Router(object):
    def __init__(self):
        self.routes = []
        self.module_base = None

    def add_route(self, template, controller, action, args=None):
        regex = re.compile('%s$' % template)
        self.routes.append((regex, controller, action, args))
    
    def load_controller(self, controller):
        module, cls = controller.rsplit('.', 1)

        cls = cls.capitalize()
        if self.module_base:
            module = '%s.%s' % (self.module_base, module)


        logging.info('IMPORTING: %s, %s' % (module, cls))
        try:
            __import__(module)
        except:
            logging.error('Could not import module %s' % module)
            raise RouterException('Could not import module %s' % module)

        if sys.modules.has_key(module):
            if hasattr(sys.modules[module], cls):
                return getattr(sys.modules[module], cls)()
            else:
                logging.error('Module has no class %s' % cls)
                raise RouterException('Module has no class %s' % cls)
        else:
            logging.error('Could not import module %s' % module)
            raise RouterException('Could not import module %s' % module)


    def __call__(self, environ, start_response):
        # going through proxy messes up path_info, so fix it
        if environ['PATH_INFO'].startswith('http'):
            host_url = environ['wsgi.url_scheme']+'://'
            if environ.get('HTTP_HOST'):
                host_url += environ['HTTP_HOST']
            else:
                host_url += environ['SERVER_NAME']
                
                if environ['wsgi.url_scheme'] == 'https':
                    if environ['SERVER_PORT'] != '443':
                        host_url += ':'+environ['SERVER_PORT']
                else:
                    if environ['SERVER_PORT'] != '80':
                        host_url += ':'+environ['SERVER_PORT']

            environ['PATH_INFO'] = environ['PATH_INFO'][len(host_url):]

        #  create webob Request
        req = Request(environ)

        # try to match a route
        logging.info('---------------------------------------------------------')
        logging.info('will test against[%s]' % req.path_url)
        for regex, controller, action, args in self.routes:
            # logging.info('testing: %s, %s, %s' % (regex.pattern, controller, action))
            matches = regex.search(req.path_url)
            if matches:
                logging.info('MATCHES!')
                # interpolate any backreferences into controller, action and args 
                controller = matches.expand(controller)
                action = matches.expand(action)
                
                # add in named groups from the regex
                req.urlvars = matches.groupdict()

                # add in unamed groups (this includes numerically keyed copies of the named groups)
                unamed_groups = matches.groups()
                req.urlvars.update(dict(zip(range(len(unamed_groups)), unamed_groups)))

                # interpolate backreferences into arg values and add to request
                if args:
                    args = dict([(k, matches.expand(v)) for k,v in args.items()])
                    req.urlvars.update(args)
                    
                # load controller
                try:
                    controller = self.load_controller(controller)
                    logging.info('GOT CTRLR: %s' % controller.__dict__)
                except RouterException, e:
                    return exc.HTTPNotFound('Could not load controller: %s' % e)(environ, start_response)
                    
                if controller:
                    # get the action
                    try:
                        action = getattr(controller, action)
                        if action:
                            try:
                                res = action(req)

                                if isinstance(res, basestring):
                                    res = Response(body=res)

                                elif not res:
                                    return exc.HTTPNotFound('No such action')(environ, start_response)

                                return res(environ, start_response)
                            except:
                                exc_info = sys.exc_info() 
                                logging.error('ERROR: %s, %s, %s' % (exc_info[0], exc_info[1], exc_info[2]))
                                logging.error(traceback.format_tb(exc_info[2]))
                                return exc.HTTPInternalServerError('Action failed: %s, %s' % (exc_info[1], traceback.format_tb(exc_info[2])))(environ, start_response)
                        else:
                            return exc.HTTPNotFound('No such action')(environ, start_response)
                    except Exception, e:
                        return exc.HTTPNotFound('No such action: %s' % e)(environ, start_response)
                else:
                    return exc.HTTPNotFound('No such controller')(environ, start_response)

        # no matches
        logging.info('NO MATCH')
        return exc.HTTPNotFound()(environ, start_response)


def serve(path, host, port):
    '''
    The main part of this configuration is the definition of routes.
    Routes have three components:
    1) a regular expression which the url is matched against (not including an query string). The regular expression is matched against the end of the url.
    2) a controller class that will handle the route. As a convienience module_base can be set to a common package prefix. Note that back references can be used here from 1). Also note that the class name will be automatically capitalized.
    3) an action; i.e. a method of the class specified in 2). This method must take one argument. This can also contain backreferences from 1)
    4) (optional) extra arguments that are passed to the action. This can contain backreferences from 1)
    '''

    router = Router()
    router.module_base = 'fusion.http.controllers'
    router.add_route('/static/(.+)',                                        'static.Static',    'index',  {'fusion.path': r'\1'})
    router.add_route('([A-Fa-f0-9]{20,40}).hexlabel.net/content/(.+)',      'content.Content',  'index',  {'fusion.hexlabel': r'\1', 'fusion.path':r'\2'})
    router.add_route('([A-Fa-f0-9]{20,40}).hexlabel.net/([^\/]+)/([^\/]+)/?', r'\2.\2',           r'\3',    {'fusion.hexlabel': r'\1'})
    router.add_route('([A-Fa-f0-9]{20,40}).hexlabel.net/([^\/]+)/?',         r'\2.\2',           'index',  {'fusion.hexlabel': r'\1'})
    router.add_route('([A-Fa-f0-9]{20,40}).hexlabel.net/?',                  'index.Index',      'index',  {'fusion.hexlabel': r'\1'})
    router.add_route('admin.hexlabel.net/([^\/]+)/?(.*)',                   'admin.Admin',      r'\1',    {'fusion.path': r'\2'})
    router.add_route('admin.hexlabel.net/',                                 'admin.Admin',      'index')
    router.add_route('my.hexlabel.net/',                                    'index.Index',      'index')
    router.add_route('/proxy/proxy.pac',                                    'proxy.Proxy',      'proxy')
    router.add_route('/proxy.pac',                                          'proxy.Proxy',      'proxy') # for backwards compatability
    router.add_route('localhost:\d+/',                                      'proxy.Proxy',      'index')

    #[TODO: is this the best way to configure middleware?]
    db = DBMiddleware(path)
    db.set_application(router)

    #[TODO: single threaded?]
    httpd = make_server(host, port, db)
    try:
        logging.info('starting httpd...')
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass

def main():
    logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S')

    parser = OptionParser()

    server_host = 'localhost'
    parser.add_option('--h', dest='server_host', default=server_host,
                      help='server host (default is %s)' % server_host)

    server_port = '8080'
    parser.add_option('--p', dest='server_port', default=server_port,
                      help='server port (default is %s)' % server_port)

    database_path = get_default_path()
    parser.add_option('--db', dest='database_path', default=database_path,
                      help='location of database (default is %s)' % database_path)

    options, args = parser.parse_args()
    if args:
        parser.error('incorrect number of arguments')

    # resolve ~ in path
    options.database_path = os.path.expanduser(options.database_path)
    if not os.path.exists(options.database_path):
        print "Database does not exist: %s" % options.database_path
        exit(1)
    else:
        logging.info('database path is: %s' % options.database_path)

    serve(options.database_path, options.server_host, int(options.server_port))


if __name__ == '__main__':
    main()
